/*
 * Copyright 2000-2006 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.intellij.gwt.module;

import com.intellij.gwt.module.model.GwtModule;
import com.intellij.gwt.module.model.impl.GwtModuleImpl;
import com.intellij.gwt.module.index.GwtFilesIndex;
import com.intellij.lang.StdLanguages;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.module.Module;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.css.*;
import com.intellij.psi.css.resolve.CssResolveManager;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.xml.DomFileDescription;
import com.intellij.util.xml.DomFileElement;
import com.intellij.util.xml.DomManager;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

/**
 * @author nik
 */
public class GwtModulesManagerImpl extends GwtModulesManager implements ProjectComponent {
  private Project myProject;
  @NonNls private static final String CSS_EXTENSION = ".css";
  private ProjectFileIndex myProjectFileIndex;
  private DomManager myDomManager;
  private GwtFilesIndex myGwtFilesIndex;

  public GwtModulesManagerImpl(final Project project, final DomManager domManager, GwtFilesIndex gwtFilesIndex) {
    myProject = project;
    myDomManager = domManager;
    myGwtFilesIndex = gwtFilesIndex;
  }

  public void projectOpened() {
  }

  public void projectClosed() {
  }

  @NonNls
  @NotNull
  public String getComponentName() {
    return "GwtModulesManager";
  }

  public void initComponent() {
    myDomManager.registerFileDescription(new DomFileDescription<GwtModule>(GwtModule.class, "module") {
      protected void initializeFileDescription() {
        registerImplementation(GwtModule.class, GwtModuleImpl.class);
      }

      public boolean isMyFile(XmlFile file, final Module module) {
        final String name = file.getName();
        return name != null && name.endsWith(GWT_XML_SUFFIX) && super.isMyFile(file, module);
      }


      public Collection<XmlFile> getMyFiles() {
        final VirtualFile[] virtualFiles = myGwtFilesIndex.getGwtXmlFiles();
        final ArrayList<XmlFile> list = new ArrayList<XmlFile>(virtualFiles.length);
        final PsiManager psiManager = PsiManager.getInstance(myProject);
        for (VirtualFile file : virtualFiles) {
          final PsiFile psiFile = psiManager.findFile(file);
          if (psiFile instanceof XmlFile) {
            list.add((XmlFile)psiFile);
          }
        }
        return list;
      }

      public boolean isAutomaticHighlightingEnabled() {
        return false;
      }
    });
    myProjectFileIndex = ProjectRootManager.getInstance(myProject).getFileIndex();
  }

  public void disposeComponent() {
    myProjectFileIndex = null;
  }


  public GwtModule[] getGwtModules() {
    final ProjectRootManager rootManager = ProjectRootManager.getInstance(myProject);
    return getGwtModules(rootManager.getFileIndex());
  }

  private GwtModule[] getGwtModules(final FileIndex fileIndex) {
    final GwtModulesFinder finder = new GwtModulesFinder(myProject);
    final VirtualFile[] files = myGwtFilesIndex.getGwtXmlFiles();
    for (VirtualFile file : files) {
      if (fileIndex.isInSourceContent(file)) {
        finder.processFile(file);
      }
    }

    final List<GwtModule> list = finder.getResults();
    return list.toArray(new GwtModule[list.size()]);
  }

  public GwtModule[] getGwtModules(final Module module) {
    final ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
    return getGwtModules(rootManager.getFileIndex());
  }

  public @Nullable GwtModule findGwtModuleByClientSourceFile(@NotNull VirtualFile file) {
    return findModuleByClientOrPublicFile(file, true, false);
  }

  public @Nullable GwtModule findGwtModuleByClientOrPublicFile(@NotNull VirtualFile file) {
    return findModuleByClientOrPublicFile(file, true, true);
  }

  private @Nullable GwtModule findModuleByClientOrPublicFile(final VirtualFile file, final boolean clientFileAllowed, final boolean publicFileAllowed) {
    final GwtModulesFinder finder = new GwtModulesFinder(myProject);
    final ProjectFileIndex index = myProjectFileIndex;
    VirtualFile parent = file.getParent();
    while (parent != null && (index.isInSource(parent) || index.isInLibraryClasses(parent))) {
      final VirtualFile[] files = parent.getChildren();
      if (files != null) {
        for (VirtualFile virtualFile : files) {
          finder.processFile(virtualFile);
        }
      }
      parent = parent.getParent();
    }

    for (GwtModule module : finder.getResults()) {
      if (clientFileAllowed) {
        final List<VirtualFile> sourceRoots = module.getSourceRoots();
        for (VirtualFile sourceRoot : sourceRoots) {
          if (VfsUtil.isAncestor(sourceRoot, file, false)) {
            return module;
          }
        }
      }
      if (publicFileAllowed) {
        final List<VirtualFile> publicRoots = module.getPublicRoots();
        for (VirtualFile publicRoot : publicRoots) {
          if (VfsUtil.isAncestor(publicRoot, file, false)) {
            return module;
          }
        }
      }
    }
    return null;
  }

  //todo[nik] return all files
  public @Nullable XmlFile findHtmlFileByModule(@NotNull GwtModule module) {
    final VirtualFile[] htmlFiles = myGwtFilesIndex.getHtmlFilesByModule(module.getQualifiedName());
    if (htmlFiles.length == 0) return null;

    final VirtualFile parent = module.getModuleDirectory();
    final VirtualFile defaultFile = parent.findFileByRelativePath(DEFAULT_PUBLIC_PATH + "/" + module.getShortName() + "." + StdFileTypes.HTML.getDefaultExtension());
    final VirtualFile htmlFile;
    if (defaultFile != null && ArrayUtil.find(htmlFiles, defaultFile) != -1) {
      htmlFile = defaultFile;
    }
    else {
      htmlFile = htmlFiles[0];
    }

    final FileViewProvider viewProvider = PsiManager.getInstance(myProject).findViewProvider(htmlFile);
    if (viewProvider == null) return null;

    return (XmlFile)viewProvider.getPsi(StdLanguages.HTML);
  }

  public @Nullable PsiElement findTagById(@NotNull XmlFile htmlFile, final String id) {
    final Map<String, XmlTag> id2Tag = getHtmlId2TagMap(htmlFile);
    return id2Tag.get(id);
  }

  private static Map<String, XmlTag> getHtmlId2TagMap(final XmlFile htmlFile) {
    final Map<String, XmlTag> id2Tag = new HashMap<String, XmlTag>();
    htmlFile.accept(new PsiRecursiveElementVisitor() {
      public void visitXmlTag(XmlTag tag) {
        final String elementId = tag.getAttributeValue("id");
        if (elementId != null) {
          id2Tag.put(elementId, tag);
        }
        super.visitXmlTag(tag);
      }
    });
    return id2Tag;
  }

  private Map<String, CssClass> buildCssClass2DeclarationMap(GwtModule module) {
    final HashMap<String, CssClass> cssClass2Declaration = new HashMap<String, CssClass>();

    final List<CssFile> list = module.getStylesheetFiles();
    for (CssFile cssFile : list) {
      final CssStylesheet stylesheet = cssFile.getStylesheet();
      if (stylesheet != null) {
        for (CssRuleset ruleset : stylesheet.getRulesets()) {
          collectDeclarations(ruleset, cssClass2Declaration);
        }
      }
    }

    final XmlFile xmlFile = findHtmlFileByModule(module);
    final CssResolveManager resolveManager = CssResolveManager.getInstance();
    for (CssRuleset cssRuleset : resolveManager.getNewResolver().resolveAll(xmlFile)) {
      collectDeclarations(cssRuleset, cssClass2Declaration);
    }
    return cssClass2Declaration;
  }

  private static void collectDeclarations(final CssRuleset cssRuleset, final HashMap<String, CssClass> cssClass2Declaration) {
    for (CssSelector selector : cssRuleset.getSelectorList().getSelectors()) {
      for (PsiElement selectorElement : selector.getElements()) {
        if (selectorElement instanceof CssSimpleSelector) {
          CssSimpleSelector simpleSelector = (CssSimpleSelector)selectorElement;
          final CssSelectorSuffix[] selectorSuffixes = simpleSelector.getSelectorSuffixes();
          for (CssSelectorSuffix suffix : selectorSuffixes) {
            if (suffix instanceof CssClass) {
              final CssClass cssClass = (CssClass)suffix;
              cssClass2Declaration.put(cssClass.getName(), cssClass);
            }
          }
        }
      }
    }
  }

  public @Nullable CssClass findCssDeclarationByClass(final GwtModule module, final String cssClass) {
    return buildCssClass2DeclarationMap(module).get(cssClass);
  }

  public String[] getAllCssClassNames(final GwtModule module) {
    final Set<String> classesSet = buildCssClass2DeclarationMap(module).keySet();
    return classesSet.toArray(new String[classesSet.size()]);
  }

  public @Nullable CssFile findPreferableCssFile(final GwtModule module) {
    final List<CssFile> list = module.getStylesheetFiles();
    if (!list.isEmpty()) {
      return list.get(0);
    }

    final XmlFile htmlFile = findHtmlFileByModule(module);
    if (htmlFile == null) {
      return null;
    }

    final CssFile[] cssFiles = CssResolveManager.getInstance().getNewResolver().resolveStyleSheets(htmlFile, null);
    final String expectedFileName = module.getShortName() + CSS_EXTENSION;
    for (CssFile cssFile : cssFiles) {
      if (expectedFileName.equals(cssFile.getName())) {
        return cssFile;
      }
    }
    if (cssFiles.length > 0) {
      return cssFiles[0];
    }
    return null;
  }

  public boolean isGwtModuleFile(final VirtualFile file) {
    return file.getName().endsWith(GWT_XML_SUFFIX) && myProjectFileIndex.isInSourceContent(file);
  }

  public boolean isInheritedOrSelf(GwtModule gwtModule, GwtModule inheritedModule) {
    final Set<GwtModule> set = new HashSet<GwtModule>();
    collectAllInherited(gwtModule, set);
    return set.contains(inheritedModule);
  }

  public boolean isLibraryModule(GwtModule module) {
    return module.getEntryPoints().isEmpty() && findHtmlFileByModule(module) == null;
  }

  private static void collectAllInherited(final GwtModule gwtModule, final Set<GwtModule> set) {
    if (!set.add(gwtModule)) return;

    for (GwtModule module : gwtModule.getInherited()) {
      collectAllInherited(module, set);
    }
  }

  public @Nullable GwtModule findGwtModuleByName(final @NotNull String qualifiedName, final GlobalSearchScope scope) {
    final GwtModule[] gwtModules = findGwtModulesByName(qualifiedName, scope);
    return gwtModules.length > 0 ? gwtModules[0] : null;
  }

  public @Nullable String getPathFromPublicRoot(@NotNull VirtualFile file) {
    final GwtModule gwtModule = findGwtModuleByClientOrPublicFile(file);
    if (gwtModule == null) return null;

    for (VirtualFile dir : gwtModule.getPublicRoots()) {
      if (VfsUtil.isAncestor(dir, file, false)) {
        return VfsUtil.getRelativePath(file, dir, '/');
      }
    }
    return null;
  }

  private GwtModule[] findGwtModulesByName(final String qualifiedName, final GlobalSearchScope scope) {
    List<GwtModule> modules = new ArrayList<GwtModule>();
    final PsiManager psiManager = PsiManager.getInstance(myProject);
    String name = qualifiedName;
    String packageName = "";
    do {
      final PsiPackage psiPackage = psiManager.findPackage(packageName);
      if (psiPackage != null) {
        final PsiDirectory[] directories = psiPackage.getDirectories(scope);
        for (PsiDirectory directory : directories) {
          final PsiFile psiFile = directory.findFile(name + GWT_XML_SUFFIX);
          if (psiFile instanceof XmlFile) {
            final DomFileElement<GwtModule> fileElement = myDomManager.getFileElement((XmlFile)psiFile);
            if (fileElement != null) {
              modules.add(fileElement.getRootElement());
            }
          }
        }
      }

      int dot = name.indexOf('.');
      if (dot == -1) break;

      final String shortName = name.substring(0, dot);
      packageName = packageName.length() > 0 ? packageName + "." + shortName : shortName;
      name = name.substring(dot + 1);
    } while(true);

    return modules.toArray(new GwtModule[modules.size()]);
  }

  public String[] getAllIds(XmlFile htmlFile) {
    final Set<String> idSet = getHtmlId2TagMap(htmlFile).keySet();
    return idSet.toArray(new String[idSet.size()]);
  }

  private static class GwtModulesFinder implements ContentIterator {
    private final List<GwtModule> myResults;
    private final PsiManager myPsiManager;
    private final DomManager myDomManager;

    public GwtModulesFinder(final Project project) {
      myResults = new ArrayList<GwtModule>();
      myPsiManager = PsiManager.getInstance(project);
      myDomManager = DomManager.getDomManager(project);
    }

    public boolean processFile(VirtualFile fileOrDir) {
      if (!fileOrDir.isDirectory() && fileOrDir.isValid()
          && fileOrDir.getFileType() == StdFileTypes.XML && fileOrDir.getNameWithoutExtension().endsWith(GWT_SUFFIX)) {
        final PsiFile psiFile = myPsiManager.findFile(fileOrDir);
        if (psiFile instanceof XmlFile) {
          final DomFileElement<GwtModule> fileElement = myDomManager.getFileElement((XmlFile)psiFile);
          if (fileElement != null) {
            myResults.add(fileElement.getRootElement());
          }
        }
      }
      return true;
    }

    public List<GwtModule> getResults() {
      return myResults;
    }
  }
}
